﻿using System;
using System.Threading.Tasks;
using ParallelMath.Library.AbstractDependencies;
using ParallelMath.Library.ConcreteDependencies;

namespace ParallelMath.Library
{
    public static class MatrixOperations2D
    {
        static MatrixOperations2D()
        {
            ThreadsCount = new ProcessorsBasedThreadsCount();
        }

        public static IThreadsCount ThreadsCount { get; set; }

        public static double[,] GenerateRandom2DMatrix(long dimension1Size, long dimension2Size, double minValue = 0, double maxValue = 1)
        {
            Random r = new Random();
            double[,] generatedMatrix = new double[dimension1Size, dimension2Size];

            for (int i = 0; i < dimension1Size; i++)
            {
                for (int j = 0; j < dimension2Size; j++)
                {
                    generatedMatrix[i, j] = r.NextDouble() * (maxValue - minValue) + minValue;
                }
            }

            return generatedMatrix;
        }

        public static double[,] Add(this double[,] matrix1, double[,] matrix2, bool executeInOneThread = false)
        {
            if (matrix1.GetLength(0) != matrix2.GetLength(0) || matrix1.GetLength(1) != matrix2.GetLength(1))
                throw new ArgumentException("The length of dimensions must be equal");

            if (Environment.ProcessorCount == 1 || executeInOneThread)
                return matrix1.AddInOneThread(matrix2);

            double[,] result = new double[matrix1.GetLength(0), matrix1.GetLength(1)];

            long elementsInOnePart = result.LongLength / MatrixOperations1D.ThreadsCount.ThreadsCount;
            long extraElementsInFirstPart = result.LongLength % MatrixOperations1D.ThreadsCount.ThreadsCount;

            long startPosition = 0;

            Task[] tasks = new Task[MatrixOperations1D.ThreadsCount.ThreadsCount];
            for (int taskNum = 0; taskNum < MatrixOperations1D.ThreadsCount.ThreadsCount; taskNum++)
            {
                long elements = elementsInOnePart;
                if (taskNum == 0)
                {
                    elements += extraElementsInFirstPart;
                }
                long position = startPosition;
                Task task = Task.Factory.StartNew(() =>
                                                      {
                                                          long index1 = 0;
                                                          long index2 = 0;

                                                          matrix1.ConvertIndexIntoArrayDimensions(position,
                                                                                                  ref index1,
                                                                                                  ref index2);
                                                          int elementsInCurrentSession = 0;

                                                          long dimension1Len = matrix1.GetLongLength(0);
                                                          long dimension2Len = matrix1.GetLongLength(1);

                                                          for (long i = index1; i < dimension1Len; i++)
                                                          {
                                                              long j = i == index1 ? index2 : 0;
                                                              for (; j < dimension2Len; j++)
                                                              {
                                                                  result[i, j] = matrix1[i, j] + matrix2[i, j];
                                                                  if (++elementsInCurrentSession == elements)
                                                                      break;
                                                              }
                                                              if (elementsInCurrentSession == elements)
                                                                  break;
                                                          }
                                                      });


                tasks[taskNum] = task;
                startPosition += elements;
            }

            Task.WaitAll(tasks);

            return result;
        }

        private static double[,] AddInOneThread(this double[,] matrix1, double[,] matrix2)
        {
            int dimension1 = matrix1.GetLength(0);
            int dimension2 = matrix1.GetLength(1);

            double[,] result = new double[dimension1, dimension2];

            for (long i = 0; i < dimension1; i++)
            {
                for (long j = 0; j < dimension2; j++)
                {
                    result[i, j] = matrix1[i, j] + matrix2[i, j];
                }
            }

            return result;
        }

        public static bool ConvertIndexIntoArrayDimensions(this double[,] matrix, long index, ref long d1, ref long d2)
        {
            if (index > matrix.LongLength)
            {
                return false;
            }
            d1 = index / matrix.GetLength(1);
            d2 = index % matrix.GetLength(1);

            return true;
        }


        public static double[,] Double2DbyANumberOneThread(this double[,] array, double number)
        {
            for (long i = 0; i < array.GetLength(0); i++)
            {
                for (int j = 0; j < array.GetLength(1); j++)
                {
                    array[i, j] = array[i, j] * number;
                }
            }
            return array;
        }

        public static double[,] Double2DbyANumber(this double[,] matrix, double number, bool executeInOneThread = false)
        {
            if (Environment.ProcessorCount == 1 || executeInOneThread)
                return matrix.Double2DbyANumberOneThread(number);


            long elementsInOnePart = matrix.LongLength / MatrixOperations1D.ThreadsCount.ThreadsCount;
            long extraElementsInFirstPart = matrix.LongLength % MatrixOperations1D.ThreadsCount.ThreadsCount;

            long startPosition = 0;

            Task[] tasks = new Task[MatrixOperations1D.ThreadsCount.ThreadsCount];
            for (int taskNum = 0; taskNum < MatrixOperations1D.ThreadsCount.ThreadsCount; taskNum++)
            {
                long elements = elementsInOnePart;
                if (taskNum == 0)
                {
                    elements += extraElementsInFirstPart;
                }
                long position = startPosition;
                Task task = Task.Factory.StartNew(() =>
                                                      {
                                                          long index1 = 0;
                                                          long index2 = 0;

                                                          matrix.ConvertIndexIntoArrayDimensions(position,
                                                                                                 ref index1,
                                                                                                 ref index2);
                                                          int elementsInCurrentSession = 0;

                                                          long dimension1Len = matrix.GetLongLength(0);
                                                          long dimension2Len = matrix.GetLongLength(1);

                                                          for (long i = index1; i < dimension1Len; i++)
                                                          {
                                                              long j = i == index1 ? index2 : 0;
                                                              for (; j < dimension2Len; j++)
                                                              {
                                                                  matrix[i, j] = matrix[i, j] * number;
                                                                  if (++elementsInCurrentSession == elements)
                                                                      break;
                                                              }
                                                              if (elementsInCurrentSession == elements)
                                                                  break;
                                                          }
                                                      });


                tasks[taskNum] = task;
                startPosition += elements;
            }

            Task.WaitAll(tasks);

            return matrix;
        }

        public static double[,] AddANumberOneThread(this double[,] array, double number)
        {
            for (long i = 0; i < array.GetLength(0); i++)
            {
                for (int j = 0; j < array.GetLength(1); j++)
                {
                    array[i, j] = array[i, j] + number;
                }
            }
            return array;
        }

        public static double[,] AddANumber(this double[,] matrix, double number, bool executeInOneThread = false)
        {
            if (Environment.ProcessorCount == 1 || executeInOneThread)
                return matrix.Double2DbyANumberOneThread(number);


            long elementsInOnePart = matrix.LongLength / MatrixOperations1D.ThreadsCount.ThreadsCount;
            long extraElementsInFirstPart = matrix.LongLength % MatrixOperations1D.ThreadsCount.ThreadsCount;

            long startPosition = 0;

            Task[] tasks = new Task[MatrixOperations1D.ThreadsCount.ThreadsCount];
            for (int taskNum = 0; taskNum < MatrixOperations1D.ThreadsCount.ThreadsCount; taskNum++)
            {
                long elements = elementsInOnePart;
                if (taskNum == 0)
                {
                    elements += extraElementsInFirstPart;
                }
                long position = startPosition;
                Task task = Task.Factory.StartNew(() =>
                {
                    long index1 = 0;
                    long index2 = 0;

                    matrix.ConvertIndexIntoArrayDimensions(position,
                                                           ref index1,
                                                           ref index2);
                    int elementsInCurrentSession = 0;

                    long dimension1Len = matrix.GetLongLength(0);
                    long dimension2Len = matrix.GetLongLength(1);

                    for (long i = index1; i < dimension1Len; i++)
                    {
                        long j = i == index1 ? index2 : 0;
                        for (; j < dimension2Len; j++)
                        {
                            matrix[i, j] = matrix[i, j] + number;
                            if (++elementsInCurrentSession == elements)
                                break;
                        }
                        if (elementsInCurrentSession == elements)
                            break;
                    }
                });


                tasks[taskNum] = task;
                startPosition += elements;
            }

            Task.WaitAll(tasks);

            return matrix;
        }
    }
}
